#region Copyright (c) 2003-2005, Luke T. Maxon

/********************************************************************************************************************
'
' Copyright (c) 2003-2005, Luke T. Maxon
' All rights reserved.
'
' Redistribution and use in source and binary forms, with or without modification, are permitted provided
' that the following conditions are met:
'
' * Redistributions of source code must retain the above copyright notice, this list of conditions and the
' 	following disclaimer.
'
' * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
' 	the following disclaimer in the documentation and/or other materials provided with the distribution.
'
' * Neither the name of the author nor the names of its contributors may be used to endorse or
' 	promote products derived from this software without specific prior written permission.
'
' THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
' WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
' PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
' ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
' LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
' INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
' OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
' IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'
'*******************************************************************************************************************/

#endregion

#define DYNAMIC_MENUS

using System;
using System.ComponentModel;
using System.Reflection;
using System.Windows.Forms;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace SuedeTest.Core {
   public delegate void EventRecordedEvent(Action action);

   ///<summary>
   /// A <see cref="Listener"/> is used by a <see cref="Recorder"/>
   /// to manage events happening on a target control.
   ///</summary>
   public class Listener : ILoggable {
      
      private System.Collections.Hashtable _listeningControlTable;
      private List<Action> _testEvents = new List<Action>();

      private bool _recordingEnabled = true;
      
      public event EventRecordedEvent EventRecorded;

      public event LoggingEventDelegate LoggingOccured;

      ///<summary>
      /// Constructs a new <see cref="Listener"/>.
      ///</summary>
      public Listener() {

      }

      public void Init() {
         registry = new SupportedEventsRegistry();
         registry.LoggingOccured += new LoggingEventDelegate(registry_LoggingOccured);
         registry.Init(this);
         _listeningControlTable = new System.Collections.Hashtable();

         FormRepository.OnFormAdded += OnNewFormAdded;
         FormRepository.OnDialogButtonClicked += (OnDialogButtonClicked);
      }

      private void registry_LoggingOccured(string s) {
         OnLoggingOccured(s);
      }

      /// <summary>
      /// Call this when the listener has something worth logging.
      /// </summary>
      /// <param name="s"></param>
      protected virtual void OnLoggingOccured(string s) {
         if (LoggingOccured != null) {
            LoggingOccured(s);
         }
      }

      private void OnDialogButtonClicked(string windowName, string buttonName) {
         EventAction ea = new EventAction(null, "DIALOG_" + buttonName, new object[] { });
         FireEvent(ea);
      }

      private void OnNewFormAdded(Form f) {
         ListenTo(f);
      }

      /// <summary>
      /// Clears all recorded test-events from this listener.
      /// </summary>
      public void ClearTest() {
         _testEvents.Clear();
      }

      private bool ControlIsMasked(object c) {
         if (c is ToolStripDropDown) {
            return true;
         }

         if (!c.GetType().IsVisible) {
            return true;
         }

         return false;
      }

      public void ListenTo(Control control) {
         if (control == null) {
            return;
         }

         if (!ControlIsMasked(control) && string.IsNullOrEmpty(control.Name)) {
            OnLoggingOccured("Warning: no control name found for type " + control.GetType().ToString() +
               " with path: " + Util.ControlUtils.BuildUnambiguousName(control));
            return;
         }

         if (control is Form) {
            Form f = control as Form;
            ListenTo(f.Menu);

#if DYNAMIC_MENUS
            if (f.Menu != null) {
               f.MenuStart += new EventHandler(MainMenu_MenuItemsFinder);
            }
#endif
         }

         AddEventListeners(control);

         ListenTo(control.ContextMenu);  //listen for ContextMenu
         ListenTo(control.ContextMenuStrip); //listen for ContextMenuStrip

         if (control is ToolStrip) {
            ToolStrip toolstrip = (ToolStrip)control;
            toolstrip.ItemAdded += new ToolStripItemEventHandler(toolstrip_ItemAdded);
            if (control.ContextMenuStrip != null) {
               ListenTo(control.ContextMenuStrip);
            }

            foreach (ToolStripItem item in toolstrip.Items) {
               if (item is ToolStripControlHost) {
                  ToolStripControlHost host = (ToolStripControlHost)item;
                  ListenTo(host.Control);
               }

               ListenTo(item);

               if (item is ToolStripDropDownItem) {
                  ToolStripDropDownItem ddItem = (ToolStripDropDownItem)item;
                  ListenTo(ddItem.DropDown);
               }
            }
         }

         control.ControlAdded += new ControlEventHandler(ControlAdded);
         control.ContextMenuChanged += new EventHandler(contextMenuChanged);

         foreach (Control c in control.Controls) {
            ListenTo(c);
         }
      }

      void toolstrip_ItemAdded(object sender, ToolStripItemEventArgs e) {
         AddEventListeners(e.Item);

         if (e.Item is ToolStripDropDownItem) {
            ListenTo((e.Item as ToolStripDropDownItem).DropDown);
         }
      }

      private void ListenTo(ToolStripItem item) {
         if (!string.IsNullOrEmpty(item.Name)) {
            AddEventListeners(item);
         }
      }

      //Listen to ToolStrip items
      private void ListenTo(ToolStripItemCollection collection) {
         if (collection != null) {
            foreach (ToolStripItem item in collection) {
               ListenTo(item);
               if (item is ToolStripControlHost) {
                  ToolStripControlHost host = (ToolStripControlHost)item;
                  ListenTo(host.Control);
               }
               if (item is ToolStripDropDownItem) {
                  ToolStripDropDownItem dropdown = (ToolStripDropDownItem)item;
                  ListenTo(dropdown.DropDownItems);
               }
            }
         }
      }

      //listen to Main Menu
      public void ListenTo(Menu menu) {
         if (menu == null) {
            return;
         }

         AddEventListeners(menu);

         if (menu.MenuItems != null) {
            foreach (MenuItem item in menu.MenuItems) {

#if DYNAMIC_MENUS
               item.Select += new EventHandler(item_Select);
#endif
               ListenTo(item);
            }
         }
      }

#if DYNAMIC_MENUS
      void item_Select(object sender, EventArgs e) {
         ListenTo((Menu)sender);
      }
#endif

#if DYNAMIC_MENUS
      /// <summary>
      /// This is a hack for MainMenus that have MenuItems added/removed dynamically:
      /// When the MainMenu for a form gets focus, traverse the MenuItem tree to find
      /// anything new.
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      void MainMenu_MenuItemsFinder(object sender, EventArgs e) {
         Form f = sender as Form;
         ListenTo(f.Menu);
      }
#endif


      private SupportedEventsRegistry registry;

      /// <summary>
      /// Adds event listeners to the specified control.
      /// </summary>
      /// <param name="control"></param>
      private void AddEventListeners(object control) {

         if (ControlIsMasked(control)) {
            return;
         }

         if (_listeningControlTable.Contains(control)) {
            //ignore previously added controls
            return;
         }

         _listeningControlTable.Add(control, null);

         EventInfo[] events = control.GetType().GetEvents();

         if (events == null) {
            return;
         }

         string controlName = control.ToString();
         if (control is Control) {
            controlName = (control as Control).Name;
         }

         foreach (EventInfo eventInfo in events) {

            MulticastDelegate recorder = registry.EventHandler(control.GetType(), eventInfo.Name);

            if (recorder != null) {
               AddEventHandlerAtStartOfChain(eventInfo, control, recorder);
            }
         }
      }

      /// <summary>
      /// This is a little complex: *many* (not all) controls in Windows Forms have
      /// a list of event listeners stored inside their protected Events property.  This
      /// property is a list of EventHandlers that is indexed BY EVENT KEY.  This event key
      /// is a private static variable inside the control (actually, most are in
      /// Control.)  For example (ignoring accessibility), "button.Events[Button.EventClick]"
      /// would give you all the listeners for that button's Click event.  Accesibility is,
      /// however, the problem here, and thus we need The Power Of Reflection (and not love)
      /// to get this done.
      /// Caveat: as mentioned, not all controls follow this convention, so we need to
      /// special case controls + eventhandlers to get this to work.
      /// </summary>
      /// <param name="eventInfo">Info for the event to add a handler to</param>
      /// <param name="control">The control to add the event to</param>
      /// <param name="recorder">The actual handler</param>
      private void AddEventHandlerAtStartOfChain(EventInfo eventInfo, object control, MulticastDelegate recorder) {
         PropertyInfo eventsProp =
         control.GetType().GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
         EventHandlerList handlers = (EventHandlerList)eventsProp.GetValue(control, null);

         IRecorder rec = (IRecorder)recorder.Target;
         FieldInfo eventKey = rec.EventKey(eventInfo.Name);

         if (eventKey == null) {
            //check if the control is a special case we are aware of
            //only special case right now is MenuItem.  We CANNOT special-case custom controls like this (without linking to that dll.)
            if (control is MenuItem) {
               AddEventHandlerAtStartOfChain(control as MenuItem, eventInfo, recorder, eventInfo.Name);
            } else {
               //see if we have a default handler we can deal with (default handlers are instance variables, private,
               //generated by the compiler with the same name as their corresponding event.
               FieldInfo fi = control.GetType().GetField(eventInfo.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
               object o = null;

               if (fi != null) {
                  o = fi.GetValue(control);
               }

               if (o is MulticastDelegate) {
                  //we found the handler!  hooray!
                  AddEventHandlerAtStartOfChain(eventInfo, o as MulticastDelegate, control, recorder);
               } else {
                  //we have nothing....
                  //default case that works a good deal of the time
                  eventInfo.AddEventHandler(control, recorder);
               }
            }
            return;
         }

         object key = eventKey.GetValue(control);

         handlers[key] = Delegate.Combine(recorder, handlers[key]);
      }

      /// <summary>
      /// Binds the handler 'recorder' to the object 'control''s handler 'handler'.  Used in cases where the Events
      /// property on the control is empty/doesn't have a corresponding key to the event.
      /// </summary>
      /// <param name="eventInfo"></param>
      /// <param name="handler"></param>
      /// <param name="control"></param>
      /// <param name="recorder"></param>
      private void AddEventHandlerAtStartOfChain(EventInfo eventInfo, MulticastDelegate handler, object control, MulticastDelegate recorder) {
         Delegate[] handlers = handler.GetInvocationList();

         foreach (Delegate d in handlers) {
            eventInfo.RemoveEventHandler(control, d);
         }

         eventInfo.AddEventHandler(control, recorder);

         foreach (Delegate d in handlers) {
            eventInfo.AddEventHandler(control, d);
         }
      }

      /// <summary>
      /// Custom handler-recorder binder, because menuItems are really screwy
      /// </summary>
      /// <param name="menuItem"></param>
      /// <param name="recorder"></param>
      /// <param name="eventName"></param>
      private void AddEventHandlerAtStartOfChain(MenuItem menuItem, EventInfo miEventInfo, MulticastDelegate recorder, string eventName) {
         //e.g., click handlers are on menuItem.data.onClick

         Type t = typeof(MenuItem);
         FieldInfo info = t.GetField("data", BindingFlags.NonPublic | BindingFlags.Instance);

         FieldInfo handlerInfo;
         object miData = info.GetValue(menuItem);
         switch (eventName) {
            case "Click":
               handlerInfo = miData.GetType().GetField("onClick", BindingFlags.NonPublic | BindingFlags.Instance);
               break;

            case "Popup":
               handlerInfo = miData.GetType().GetField("onPopup", BindingFlags.NonPublic | BindingFlags.Instance);
               break;

            case "Select":
               handlerInfo = miData.GetType().GetField("onSelect", BindingFlags.NonPublic | BindingFlags.Instance);
               break;

            default:
               System.Diagnostics.Debug.Assert(false, "Unhandled event handler for MenuItem: " + eventName);
               return;
         }

         System.EventHandler handler = (System.EventHandler)handlerInfo.GetValue(miData);

         if (handler == null) {
            //no-one is listening to this handler, so we can be the first
            handlerInfo.SetValue(miData, recorder);
         } else {
            //remove all old handlers, add us as first, put 'em all back
            Delegate[] delgs = handler.GetInvocationList();

            foreach (Delegate d in delgs) {
               miEventInfo.RemoveEventHandler(menuItem, d);
            }

            miEventInfo.AddEventHandler(menuItem, recorder);

            foreach (Delegate d in delgs) {
               miEventInfo.AddEventHandler(menuItem, d);
            }
         }
      }

      public void FireEvent(object control, string name, params object[] args) {
         AddEvent(new EventAction(control, name, args));
      }

      public void FireEvent(EventAction action) {
         AddEvent(action);
      }

      public void FireEvent(PropertyAssertAction action) {
         AddEvent(action);
      }

      /// <summary>
      /// Final sink for recorded events.
      /// </summary>
      /// <param name="action"></param>
      public void AddEvent(Action action) {
         if (_recordingEnabled) {
            _testEvents.Add(action);
            if (EventRecorded != null) {
               EventRecorded(action);
            }
         }
      }

      /// <summary>
      /// Returns the list of test actions associated with this listener.
      /// </summary>
      public ReadOnlyCollection<Action> TestActions {
         get {
            return _testEvents.AsReadOnly();
         }
      }

      /// <summary>
      /// Returns true iff this listener is recording test actions.
      /// </summary>
      public bool IsRecording {
         get {
            return _recordingEnabled;
         }
      }

      /// <summary>
      /// Enables this listener to record test actions.
      /// </summary>
      public void StartRecording() {
         _recordingEnabled = true;
      }

      /// <summary>
      /// Stops this listener from recording test actions.
      /// </summary>
      public void StopRecording() {
         _recordingEnabled = false;
      }

      private void ControlAdded(object sender, ControlEventArgs e) {
         ListenTo(e.Control);
      }

      private void contextMenuChanged(object sender, EventArgs e) {
         //ListenToContextMenu((Control)sender);
      }

      private void ListenToContextMenu(Control control) {
         ListenTo(control.ContextMenu);
      }
   }
}
